Um cliente para a ‘API WebDriver’: controlando um navegador da web. Funciona com qualquer implementação de ‘WebDriver’, mas foi testado apenas com ‘PhantomJS’.
Selenium é uma ferramenta de código aberto popular para automação de navegadores web, e utiliza a API WebDriver, uma interface padrão para controle de navegadores, que também é acessível em R por meio do pacote “webdriver”, possibilitando a automação de navegadores para testes e raspagem de dados na web.
Código
library(webdriver)## Define Main URLurl <-"https://sigaa.unb.br/sigaa/public/componentes/busca_componentes.jsf"## Init Session and start scraping by query typeinit_session <-function(type_of_query,url){# Init Library, Session and Navigate to URL pjs <<-run_phantomjs() s <<- Session$new(port = pjs$port) s$go(url)# First Search Boxes## Select "Graduação" search_nivel <- s$findElement(css ="option[value='G']") search_nivel$click()## Select "Disciplinas" search_tipo <- s$findElement(xpath ='//select[@id="form:tipo"]//option[@value="2"]') search_tipo$click()# Search only in the following 'unidades' according to `type_of_query`## - DEPARTAMENTO## - DEPTO## - FACULDADE## - INSTITUTO switch(type_of_query,## Select "Departamentos"departamentos = { query <- s$findElements(xpath ='//select[@id="form:unidades"]//option[starts-with(text(),"DEPARTAMENTO") or starts-with(text(),"DEPTO")]')},## Select "Faculdades"faculdades = { query <- s$findElements(xpath ='//select[@id="form:unidades"]//option[starts-with(text(),"FACULDADE")]')},## Select "Institutos"institutos = { query <- s$findElements(xpath ='//select[@id="form:unidades"]//option[starts-with(text(),"INSTITUTO") ]')} )return(query)}## Functions iterate_unit_list <-function(unit_list){if(!is.list(unit_list)) {message("Argument is not a list")return() }message( glue::glue("Process started at {Sys.time()} ")) total_size <-length(unit_list)# DEFINE SCRAPING RANGE HERE ## - uncomment the next line, change to desired range# - comment the other for, or use it uncommented to get ALL data at once.#for(i inc(43:44)){# Submit full search criteriabrowser()message( glue::glue("Scanning ",unit_list[[i]]$getText()," [{i}/{total_size}]")) unit_list[[i]]$click() submit_search <- s$findElement(xpath ='//input[@id="form:btnBuscarComponentes"]') submit_search$click()## Create the index and start scanning if not empty. details_list <- s$findElements(xpath ="//a[contains(@title, 'Detalhes')]" )if(length(details_list) ==0){message("No subjects found: Continuing with next unit... ")# Rebuild Index unit_list <- s$findElements(xpath ='//select[@id="form:unidades"]//option[starts-with(text(),"DEPARTAMENTO") or starts-with(text(),"DEPTO")]')next } content <-get_subject_content(details_list)# Write this Unit CSV to persist dataexport_content(content)# Rebuild Index unit_list <- s$findElements(xpath ='//select[@id="form:unidades"]//option[starts-with(text(),"DEPARTAMENTO") or starts-with(text(),"DEPTO")]') }message(glue::glue("Process Completed at {Sys.time()}"))return()}get_subject_content <-function(subject_list){if(!is.list(subject_list)) {message("Argument is not a list")return() } total_size <-length(subject_list) tables <- tibble::tibble()for(i in1:length(subject_list)){# Go to detail page subject_list[[i]]$click()# Grab its table in a nice format## The '25' is a khabalistic-pseudo-safe number that I suppose will be safe enough## to get any number of other non-standard fields and still get the last one with the "Ementa" table <- s$findElements(xpath ="//table[@class='visualizacao']/tbody/tr[position() <= 25]/td[not(table)]/..") details <- table |> purrr::reduce(\(acc,e){ paste0(acc, e$executeScript("return arguments[0].outerHTML")) }, .init ="<table>") |>append("</table>") |>paste0(collapse="") |> rvest::read_html() |> rvest::html_table() |> purrr::pluck(1) |> dplyr::filter( stringr::str_starts(X1,"Tipo do Componente") | stringr::str_starts(X1,"Modalidade") | stringr::str_starts(X1,"Unidade") | stringr::str_starts(X1,"Código") | stringr::str_starts(X1,"Nome") | stringr::str_starts(X1,"Pré-Requisitos") | stringr::str_starts(X1,"Co-Requisitos") | stringr::str_starts(X1,"Equivalências") | stringr::str_starts(X1,"Ementa") ) |> tidyr::pivot_wider(names_from=X1, values_from=X2, values_fill=NA)message("Details ✅")# Grab Workload find_workload <- \(path){s$findElement(xpath = path)$getText()} possibly_find_workload <- purrr::possibly(find_workload, otherwise ="Subtotal de Carga Horária de Aula - Presencial \n0h") workload <-possibly_find_workload("//table[@class='visualizacao']//td[b[contains(text(),'Subtotal de Carga Horária de Aula - Presencial')]]/following-sibling::td/..") workload_df <- workload |> stringr::str_split("\n", simplify =TRUE) |> (\(.workload){ tibble::tibble( "{.workload[1]}":= .workload[2])})()# Concatenate Details and Workload details_df <- tibble::add_column(details, workload_df)message("Workload ✅")##### Go back to index page #### back <- s$findElement(xpath ="//a[text()=' << Voltar ']") back$click()# Try to get detailed program program_list <- s$findElements(xpath ="//a[contains(@title, 'Programa')]" )tryCatch( { program_list[[i]]$click() program <- s$findElements(css =".itemPrograma") }, # If fails, close error modal and rebuild indexerror =function(e) { message("No program available. Going Back...") error <- s$findElement(css ="#fechar-painel-erros > a") error$click() },finally = program_list <- s$findElements(xpath ="//a[contains(@title, 'Programa')]" ) )# If succeeds, process program data and go back to main page.if(length(program) >0) { program_df <- tibble::tibble(Objetivos = program[[1]]$getText(), "Conteúdo"= program[[2]]$getText()) s$goBack() } else { program_df <- tibble::tibble(Objetivos =NA, "Conteúdo"=NA) }message("Program ✅")# Recreate the index subject_list <- s$findElements(xpath ="//a[contains(@title, 'Detalhes')]" ) program_list <- s$findElements(xpath ="//a[contains(@title, 'Programa')]" )# Concatenate to final structure full_table_df <- tibble::add_column(details_df, program_df) tables <- tibble::add_row(full_table_df, tables) message(glue::glue("Subject {i} of {total_size} scraped!")) }return(tables)}# Export final data frame to CSVexport_content <-function(content) {print(content) janitor::clean_names(content) |> (\(df){ clean_name <- stringr::str_remove_all(df$unidade_responsavel[1], "[:digit:]") |> janitor::make_clean_names() print(clean_name)write.csv(df, file =paste0(clean_name,".csv"), row.names =FALSE) } )()}
Todas as disciplinas dos Institutos, Faculdades e departamentos da UnB (>8000).
Escolhemos analisar: Instituto de Exatas, Faculdade de Tecnologia, Física, Química e Economia.
ChatGPT
Reinforcement learning
What is reinforcement learning?
Reinforcement learning é uma área de aprendizado de máquina que se preocupa com a forma como os agentes do software devem agir em um ambiente para maximizar a noção de recompensa cumulativa
Trata-se da introdução de um viés humano no modelo de linguagem
Processo de aprendizagem do GPT
O GPT usa a técnica de Reinforcement Learning from Human Feedback (RLHF) para minimizar saídas perigosas, falsas ou viesadas do modelo. É um processo em três fases:
Modelo Supervised Fine-Tuning (SFT) [12-15k data points]
Reward model (RM) [30-40k prompts]
Fine-tuning do modelo SFT via Proximal Policy Optimization (PPO)
onde \(r_t(\theta) = \frac{\pi_\theta}{\pi_{\theta_{old}}}\), \(\pi\) se refere a uma política, \(A^t\) é um advantage estimator e \(\epsilon\) é um hiperparâmetro pequeno
A função clip garante que a razão entre as políticas não desvie significativamente do intervalo \([1 - \epsilon, 1 + \epsilon]\)
Uma política \(\pi(s)\) compreende as ações sugeridas que o agente deve realizar para cada estado possível \(s \in S\), seguindo um Markov decision process.
Proximal Policy Optimization (PPO)
O PPO é um algoritmo que otimiza a função de perda de uma política de aprendizagem por reforço usado na OpenAI desde 2017
O PPO busca um equilíbrio entre a facilidade de implementação, a complexidade da amostragem e a facilidade de ajuste
Tenta calcular uma atualização em cada etapa que minimize a função de custo e, ao mesmo tempo, garantindo que o desvio da política anterior seja relativamente pequeno.
Como conectou com a API
Código
# importsfrom openai import OpenAIimport pandas as pdimport timefrom tqdm import tqdmfrom multiprocessing import Pool, cpu_countimport osfrom wakepy import keep# setupapi_key =open('api_key.txt').read().strip()client = OpenAI(api_key=api_key)obj_len =150prompt =f"""Gostaria de usar o texto base para fazer uma descrição mais inteligível, direta, e bem explicada de no máximo {obj_len} palavras que vou chamar de Ementa Padronizada. Para que você tenha um contexto, o texto que estou te fornecendo corresponde a uma disciplina de nível de graduação em uma universidade federal no brasil. Eu gostaria dessa ementa em um texto corrido e não em tópicos. Também gostaria que excluísse descrição de processos avaliativos usados e que se atenha a descrição do conteúdo que será ensinado de modo geral. Não é necessário detalhar como será dividido o conteúdo como módulos/unidade ou equivalentes, apenas a descrição do conteúdo abordado de forma geral. Peço também que não faça referencias ao fato de ser uma ementa padronizada, e retorne apenas o texto que voce produzir e nada mais.""".strip().replace('\n', '')def process_row(row):try: response = client.chat.completions.create( model="gpt-4", messages=[ {"role": "system", "content": prompt}, {"role": "user", "content": row['conteudo_completo']}, ] ).choices[0].message.contentreturn responseexceptExceptionas e:print(f"Error: {e}")returnNonedef init_worker():print(f"Initializing worker process {os.getpid()}")def save_progress(ementas, filename='ementas.csv'): ementas.to_csv(filename, index=False)# print("Progress saved to disk.")if__name__=="__main__": ementas = pd.read_csv('ementas.csv')if'conteudo_padronizado_gpt4'notin ementas.columns: ementas['conteudo_padronizado_gpt4'] = pd.Series(index=ementas.index, dtype=str) departments = ['INSTITUTO DE CIÊNCIAS EXATAS','DEPTO ENGENHARIA FLORESTAL','DEPTO ENGENHARIA DE PRODUCAO','DEPTO ESTATÍSTICA','DEPARTAMENTO DE MATEMÁTICA','DEPTO ECONOMIA','DEPTO CIÊNCIAS DA COMPUTAÇÃO','DEPTO ENGENHARIA CIVIL E AMBIENTAL','DEPTO ENGENHARIA ELETRICA','INSTITUTO DE QUÍMICA','INSTITUTO DE FÍSICA','FACULDADE DE TECNOLOGIA' ] ementas = ementas[ementas['unidade_responsavel'].isin(departments)] pool_size = cpu_count() pool = Pool(pool_size, initializer=init_worker) missing_rows = ementas[ementas['conteudo_padronizado_gpt4'].isna()]with keep.running():for i, row in tqdm(missing_rows.iterrows(), total=len(missing_rows)): result = pool.apply_async(process_row, args=(row,)) ementas.loc[i, 'conteudo_padronizado_gpt4'] = result.get()if i %32==0: save_progress(ementas) save_progress(ementas) pool.close() pool.join()
O que usamos dos dados
Nome + ementa + descrição + conteúdo - > Minúscula, sem acentos/carac. especiais
Prompt usado
Gostaria de usar o texto base para fazer uma descrição mais inteligível, direta, e bem explicada de no máximo 150 palavras que vou chamar de Ementa Padronizada. Para que você tenha um contexto, o texto que estou te fornecendo corresponde a uma disciplina de nível de graduação em uma universidade federal no brasil. Eu gostaria dessa ementa em um texto corrido e não em tópicos. Também gostaria que excluísse descrição de processos avaliativos usados e que se atenha a descrição do conteúdo que será ensinado de modo geral. Não é necessário detalhar como será dividido o conteúdo como módulos/unidade ou equivalentes, apenas a descrição do conteúdo abordado de forma geral. Peço também que não faça referencias ao fato de ser uma ementa padronizada, e retorne apenas o texto para que seja fácil copiá-lo.
VIGILANCIA SANITARIA, DEONTOLOGIA E LEGISLACAO FARMACEUTICA
Esta disciplina de nível de graduação em farmácia foca na vigilância sanitária, deontologia e legislação farmacêutica. O curso visa introduzir o estudante à legislação atual que rege a produção, comercialização, prescrição, informação e dispensação de medicamentos. Também é abordada a legislação do sistema de saúde e da vigilância sanitária, além de se destacar os aspectos éticos da profissão farmacêutica.
O conteúdo inclui uma exploração da história da profissão farmacêutica, a evolução do conceito de ética profissional, e as regulamentações que influenciam a prática farmacêutica. Os alunos são incentivados a desenvolver uma reflexão crítica sobre os dilemas éticos da profissão. O curso também proporciona um entendimento sobre vigilância sanitária, incluindo seu papel no sistema de saúde, o processo de registro de medicamentos, e as práticas relacionadas à informação e propaganda de medicamentos.
Além disso, o curso abrange temas como práticas de produção e inspeção farmacêutica, a defesa do consumidor em relação a medicamentos, e o controle de qualidade laboratorial dentro do contexto da vigilância sanitária. O objetivo é preparar os alunos para compreender e aplicar as leis e regulamentos do campo farmacêutico, fomentando uma prática ética e responsável.
Embeding
O “embedding” é uma técnica em aprendizado de máquina que transforma dados complexos e de alta dimensão, como textos ou imagens, em vetores de baixa dimensão, preservando as relações semânticas e contextuais.
API OpenAI
A API da OpenAI foi utilizada para fazer os prompts de padronização dos textos e geração dos embeddings usando ADA version-002
Contexto mais longo. O comprimento do contexto do novo modelo é aumentado por um fator de quatro, de 2048 para 8192, tornando-o mais conveniente para trabalhar com documentos extensos.
K-means:
O k-Means é um algoritmo de agrupamento que divide dados em ( k ) grupos, minimizando a variação interna e ajustando os centróides de cada grupo iterativamente até a convergência.
t-SNE
O t-SNE (t-distributed stochastic neighbor embedding) é uma técnica de abordagem não-linear de redução de dimensionalidade, focado na preservação das semelhanças locais, ideal para visualizar agrupamentos em duas ou três dimensões ( Geoffrey Hinton & Sam Roweis). Variação usando a t-Student por Laurens van der Maaten.
Clusterização
Resultados fodásticos
import numpy as npimport pandas as pdimport plotly as pltfrom ast import literal_evalfrom sklearn.cluster import KMeansfrom sklearn.manifold import TSNEdf = pd.read_csv('ementas.csv')df["embeddings_ada"] = df.embeddings_ada.apply(literal_eval).apply(np.array)matrix = np.vstack(df.embeddings_ada.values)matrix.shapen_clusters =len(df['unidade_responsavel'].unique())kmeans = KMeans(n_clusters=n_clusters, init="k-means++", random_state=42)kmeans.fit(matrix)labels = kmeans.labels_df["Cluster"] = labelscluster_names = []for i inrange(n_clusters):print(f'Cluster {i}') names =' '.join(df[df['Cluster'] == i]['nome'].to_list())print(len(names)) cluster_names.append(names)print(names) generated_names = ['Ciências Físicas Avançadas','Economia e Política Econômica','Estatística e Métodos Quantitativos','Ciência da Computação e Sistemas','Engenharia Civil e Infraestrutura','Matemática Avançada e Aplicada','Gestão e Projeto Interdisciplinar','Engenharia de Redes e Telecomunicações','Gestão Ambiental e Sustentabilidade','Química Teórica e Aplicada','Estágio Supervisionado e Regência','Engenharia Elétrica e Eletrônica' ]fig, ax = plt.subplots(figsize=(15, 5)) tsne = TSNE(n_components=2, perplexity=15, random_state=42, init="random", learning_rate=200) vis_dims2 = tsne.fit_transform(matrix) x = [x for x, y in vis_dims2] y = [y for x, y in vis_dims2] deps = df['unidade_responsavel'].unique().tolist()for category, color inenumerate([plt.get_cmap("tab20")(i) for i inrange(n_clusters)]): xs = np.array(x)[df.unidade_responsavel == deps[category]] ys = np.array(y)[df.unidade_responsavel == deps[category]] ax.scatter(xs, ys, color=color, alpha=0.2) avg_x = xs.mean() avg_y = ys.mean() ax.annotate( deps[category], (avg_x, avg_y), horizontalalignment='center', verticalalignment='center', size=10, weight='bold', color=color, alpha=1 ) ax.set_title("Visualização do Embedding dos Departamentos usando t-SNE") plt.show()
t-SNE Departamentos
t-SNE Departamentos
t-SNE Estatística
t-SNE Economia
Protótipo
import pandas as pdimport numpy as npfrom ast import literal_evalfrom sklearn.metrics.pairwise import cosine_similarityfrom openai import OpenAIimport gradio as grapi_key =open('api_key.txt').read().strip()client = OpenAI(api_key=api_key)df = pd.read_csv('ementas.csv')df["embeddings_ada"] = df.embeddings_ada.apply(literal_eval).apply(np.array)def get_embedding(text, model="text-embedding-ada-002"): text = text.replace("\n", " ")return client.embeddings.create(input=[text], model=model).data[0].embeddingdef get_recommendations(text): text_embedding = np.array(get_embedding(text)).reshape(1, -1) similarity = df['embeddings_ada'].apply(lambda x: cosine_similarity(x.reshape(1, -1), text_embedding.reshape(1, -1)).item()) similarity = similarity.sort_values(ascending=False).head(10) similarity = df.iloc[similarity.index].drop_duplicates(subset=['nome']).drop(columns=['conteudo_completo', 'embeddings_ada']) similarity.index =range(1, len(similarity)+1)return similaritydef recommend(text):try: recommendations = get_recommendations(text)# Convert the DataFrame to HTML for renderingreturn recommendations.to_html(escape=False)exceptExceptionas e:returnstr(e)with gr.Blocks() as demo: gr.Markdown("## Course Recommendation System") gr.Markdown("Entre suas preferências de acadêmicas e nós te recomendaremos os 10 cursos mais similares da área de exatas + engenharias.")with gr.Row(): text_input = gr.Textbox(lines=2, placeholder="Enter Description Here", label="Descreva seus interesses acadêmicos")with gr.Row(): submit_button = gr.Button("Submit") output = gr.HTML() submit_button.click(recommend, inputs=text_input, outputs=output)demo.launch()
Conclusões e Recomendações futuras
É difícil conseguir os dados da UnB.
Dá pra fazer WebScraping no R
A API da OpenAI é ótima (e cara!)
Da pra usar outros modelos além do ChatGPT
Foi possível criar clusters relativamente coesos.
Analisar quais as disciplinas “distoantes”.
Dashboard para a consulta por parte dos alunos.
Análise mais formal de sobreposição entre cursos ou falta de coesão no currículo.